package com.smap.proj;

/**
 * This is ported from gdal2tiles::GlobalMercator Python source code
 * 
 * @author Shen
 * 
 */
public class GlobalMercator {
	public static final int TILE_SIZE = 256;
	private final int tileSize;
	private final double initialResolution;
	private final double originShift;

	public GlobalMercator() {
		tileSize = TILE_SIZE;
		initialResolution = 2 * Math.PI * 6378137 / tileSize;

		originShift = 2 * Math.PI * 6378137 / 2.0;

	}

	public double[] LatLonToMeters(double lat, double lon) {

		double mx = lon * originShift / 180.0;
		double my = Math.log(Math.tan((90 + lat) * Math.PI / 360.0))
				/ (Math.PI / 180.0);
		my = my * originShift / 180.0;
		return new double[] { mx, my };
	}

	public double[] MetersToLatLon(double mx, double my) {

		double lon = (mx / originShift) * 180.0;
		double lat = (my / originShift) * 180.0;

		lat = 180
				/ Math.PI
				* (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0);
		return new double[] { lat, lon };
	}

	public double[] PixelsToMeters(double px, double py, int zoom) {
		double res = Resolution(zoom);
		double mx = px * res - originShift;
		double my = py * res - originShift;
		return new double[] { mx, my };
	}

	public int[] MetersToPixels(double mx, double my, int zoom) {
		double res = Resolution(zoom);
		int px = (int) Math.round((mx + originShift) / res);
		int py = (int) Math.round((my + originShift) / res);
		return new int[] { px, py };
	}

	public int[] PixelsToTile(int px, int py) {
		int tx = (int) Math.ceil(px / ((double) tileSize) - 1);
		int ty = (int) Math.ceil(py / ((double) tileSize) - 1);
		return new int[] { tx, ty };
	}

	public int[] PixelsToRaster(int px, int py, int zoom) {
		int mapSize = tileSize << zoom;
		return new int[] { px, mapSize - py };
	}

	public int[] MetersToTile(double mx, double my, int zoom) {
		int[] p = MetersToPixels(mx, my, zoom);
		return PixelsToTile(p[0], p[1]);
	}

	public double[] TileBounds(int tx, int ty, int zoom) {
		double[] min = PixelsToMeters(tx * tileSize, ty * tileSize, zoom);
		double minx = min[0], miny = min[1];
		double[] max = PixelsToMeters((tx + 1) * tileSize, (ty + 1) * tileSize,
				zoom);
		double maxx = max[0], maxy = max[1];
		return new double[] { minx, miny, maxx, maxy };
	}

	public double[] TileLatLonBounds(int tx, int ty, int zoom) {
		double[] bounds = TileBounds(tx, ty, zoom);
		double[] mins = MetersToLatLon(bounds[0], bounds[1]);
		double[] maxs = MetersToLatLon(bounds[2], bounds[3]);
		return new double[] { mins[0], mins[1], maxs[0], maxs[1] };
	}

	public double Resolution(int zoom) {

		return initialResolution / Math.pow(2, zoom);
	}

	public int ZoomForPixelSize(int pixelSize) {
		for (int i = 0; i < 30; i++) {
			if (pixelSize > Resolution(i)) {
				if (i != 0) {
					return i - 1;
				} else {
					return 0;
				}
			}
		}
		return 0;
	}

	public int[] GoogleTile(int tx, int ty, int zoom) {

		return new int[] { tx, (int) ((Math.pow(2, zoom) - 1) - ty) };
	}

	public int[] TMSTileFromGoogleTile(int tx, int ty, int zoom) {

		return new int[] { tx, (int) ((Math.pow(2, zoom) - 1) - ty) };
	}

	public int[] GoogleTile(double lat, double lon, int zoom) {
		double[] meters = LatLonToMeters(lat, lon);
		int[] tile = MetersToTile(meters[0], meters[1], zoom);
		return this.GoogleTile(tile[0], tile[1], zoom);
	}

	public String QuadTree(int tx, int ty, int zoom) {
		String quadKey = "";
		ty = (int) ((Math.pow(2, zoom) - 1) - ty);
		for (int i = zoom; i < 0; i--) {
			int digit = 0;
			int mask = 1 << (i - 1);
			if ((tx & mask) != 0) {
				digit += 1;
			}
			if ((ty & mask) != 0) {
				digit += 2;
			}
			quadKey += (digit + "");
		}
		return quadKey;
	}

	/**
	 * this work for google and osm, looks like they are same about tiles
	 * caculation
	 * 
	 * @param lat
	 * @param lon
	 * @param zoom
	 * @return
	 */
	public static int[] getTileNumber(final double lat, final double lon,
			final int zoom) {
		int xtile = (int) Math.floor((lon + 180) / 360 * (1 << zoom));
		int ytile = (int) Math
				.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1
						/ Math.cos(Math.toRadians(lat)))
						/ Math.PI)
						/ 2 * (1 << zoom));
		int[] ret = new int[] { xtile, ytile };
		return ret;
	}

	static GlobalMercator instance;

	public static GlobalMercator get() {
		if (instance == null) {
			instance = new GlobalMercator();
		}
		return instance;
	}

	public static void main(String[] args) {
		int zoom = 10;
		double lon = 144.96;
		double lat = -37.81;
		System.out.println("http://tile.openstreetmap.org/"
				+ getTileNumber(lat, lon, zoom) + ".png");

		GlobalMercator mercator = new GlobalMercator();
		int[] tile = mercator.GoogleTile(lat, lon, zoom);
		System.out.println(tile[0] + "," + tile[1]);
	}
}
